﻿using System;
using System.Activities;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Build.Workflow.Activities;
using Microsoft.TeamFoundation.Build.Workflow.Tracking;


namespace Qetza.TeamFoundation.Build.Workflow.Activities
{
    /// <summary>
    /// Activity to updates the assembly version and/or assembly file version in a file.
    /// </summary>
    [BuildActivity(HostEnvironmentOption.All)]
    public sealed class UpdateAssemblyInfoFile : CodeActivity
    {
        #region Fields

        // regular expression to find tokens.
        private static readonly Regex g_tokensRegex = new Regex(@"\$\((?<token>[^:\)]*)(:(?<param>[^\)]+))?\)", RegexOptions.IgnoreCase);

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the file path.
        /// </summary>
        /// <value>
        /// The file path.
        /// </value>
        public InArgument<string> FilePath { get; set; }

        /// <summary>
        /// Gets or sets the assembly version format.
        /// </summary>
        /// <value>
        /// The assembly version format.
        /// </value>
        public InArgument<string> AssemblyVersionFormat { get; set; }

        /// <summary>
        /// Gets or sets the assembly file version format.
        /// </summary>
        /// <value>
        /// The assembly file version format.
        /// </value>
        public InArgument<string> AssemblyFileVersionFormat { get; set; }

        /// <summary>
        /// Gets or sets the assembly informational version format.
        /// </summary>
        /// <value>
        /// The assembly informational version format.
        /// </value>
        public InArgument<string> AssemblyInformationalVersionFormat { get; set; }

        /// <summary>
        /// Gets or sets the updated assembly version.
        /// </summary>
        /// <value>
        /// The updated assembly version.
        /// </value>
        public OutArgument<Version> AssemblyVersion { get; set; }

        /// <summary>
        /// Gets or sets the updated assembly file version.
        /// </summary>
        /// <value>
        /// The updated assembly file version.
        /// </value>
        public OutArgument<Version> AssemblyFileVersion { get; set; }

        /// <summary>
        /// Gets or sets the updated assembly informational version.
        /// </summary>
        /// <value>
        /// The updated assembly informational version.
        /// </value>
        public OutArgument<string> AssemblyInformationalVersion { get; set; }

        #endregion

        #region Methods

        /// <summary>
        /// Executes the activity and update the assembly file informations.
        /// </summary>
        /// <param name="context">
        /// The execution context.
        /// </param>
        protected override void Execute(CodeActivityContext context)
        {
            // set context
            this.Context = context;

            // load file
            var filePath = this.FilePath.Get(this.Context);
            var text = File.ReadAllText(filePath);

            // update assembly version
            var now = DateTime.Now;
            var format = this.AssemblyVersionFormat.Get(this.Context);
            text = UpdateVersion(text, "AssemblyVersion", format, now, this.AssemblyVersion);

            // update assembly file version
            format = this.AssemblyFileVersionFormat.Get(this.Context);
            text = UpdateVersion(text, "AssemblyFileVersion", format, now, this.AssemblyFileVersion);

            // update assembly informational version
            format = this.AssemblyInformationalVersionFormat.Get(this.Context);
            text = UpdateInformationalVersion(text, format, now);

            // write to file (unset and set back ReadOnly attribute if present).
            var fileAttributes = File.GetAttributes(filePath);
            var attributesChanged = false;

            if ((fileAttributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
            {
                File.SetAttributes(filePath, fileAttributes ^ FileAttributes.ReadOnly);
                attributesChanged = true;
            }

            File.WriteAllText(filePath, text);

            if (attributesChanged)
            {
                File.SetAttributes(filePath, FileAttributes.ReadOnly);
            }
        }

        #endregion

        #region Private Helpers

        // The current context.
        private CodeActivityContext Context { get; set; }

        // Update the version of the specified attribute.
        private string UpdateVersion(
            string text, 
            string attribute, 
            string format, 
            DateTime date, 
            OutArgument<Version> outVersion
            )
        {
            // find attribute
            var regex = new Regex(@"(?<attribute>assembly:?\s*" + attribute + @"\s*)\(""(?<value>\d+\.\d+\.\d+\.\d+)""\)", RegexOptions.IgnoreCase);
            var match = regex.Match(text);

            if (match.Success)
            {
                // extract current version
                var version = new Version(match.Groups["value"].Value);
                outVersion.Set(this.Context, version);

                if (!string.IsNullOrWhiteSpace(format))
                {
                    // extract tokens
                    var tokens = format.Split('.');
                    if (tokens.Length != 4)
                    {
                        throw new FormatException("Version format was not in the correct format.");
                    }

                    // generate new version
                    version = new Version(
                        Convert.ToInt32(ReplaceTokens(tokens[0], version.Major, date)),
                        Convert.ToInt32(ReplaceTokens(tokens[1], version.Minor, date)),
                        Convert.ToInt32(ReplaceTokens(tokens[2], version.Build, date)),
                        Convert.ToInt32(ReplaceTokens(tokens[3], version.Revision, date))
                        );

                    outVersion.Set(this.Context, version);

                    return regex.Replace(text, string.Format("{0}(\"{1}\")", match.Groups["attribute"].Value, version));
                }
            }

            // return unmodified text
            return text;
        }

        // Update the informational version.
        private string UpdateInformationalVersion(
            string text,
            string format,
            DateTime date
            )
        {
            // find attribute
            var regex = new Regex(@"(?<attribute>assembly:?\s*AssemblyInformationalVersion\s*)\(""(?<value>.*(?!\)))""\)", RegexOptions.IgnoreCase);
            var match = regex.Match(text);

            if (match.Success)
            {
                // extract current version
                var version = match.Groups["value"].Value;
                this.AssemblyInformationalVersion.Set(this.Context, version);

                if (!string.IsNullOrWhiteSpace(format))
                {
                    // generate new version
                    version = ReplaceTokens(format, default(int), date);

                    this.AssemblyInformationalVersion.Set(this.Context, version);

                    return regex.Replace(text, string.Format("{0}(\"{1}\")", match.Groups["attribute"].Value, version));
                }
            }

            // return unmodified text
            return text;
        }

        // Expands the specified token.
        private string ReplaceTokens(string value, int current, DateTime date)
        {
            // define replace functions
            var evaluators = new Dictionary<string, Func<string, string>>
            {
                { "current", p => current.ToString() },
                { "increment", p => (++current).ToString() },
                { "date", p => date.ToString(p, System.Globalization.CultureInfo.CurrentCulture) },
                { "version", p => this.AssemblyVersion.Get(this.Context).ToString() },
                { "fileversion", p => this.AssemblyFileVersion.Get(this.Context).ToString() }
            };

            // replace tokens
            return g_tokensRegex.Replace(
                value,
                m =>
                {
                    var evaluator = default(Func<string, string>);
                    if (!evaluators.TryGetValue(m.Groups["token"].Value, out evaluator))
                    {
                        throw new FormatException("Unknown token " + m.Groups["token"].Value + ".");
                    }

                    return evaluator(m.Groups["param"].Value);
                });
        }

        #endregion
    }
}
